Skip to content

local echo (7/7): Support simplified version of local echo #1453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

PIG208
Copy link
Member

@PIG208 PIG208 commented Apr 1, 2025

Fixes #1441.

Stacked atop #1472.

(This branch can be used to preview the full implementation)

screenshots
message sent got message event
sending-success-before sending-success-after
message failed to send message content tapped recipient headers
send-failed sent-failed-interacted recipient-headers

@PIG208 PIG208 changed the title Support simplified version a local echo (n/n) Support simplified version of local echo (n/n) Apr 1, 2025
@PIG208 PIG208 changed the title Support simplified version of local echo (n/n) locale echo (n/n): Support simplified version of local echo Apr 1, 2025
@PIG208 PIG208 changed the title locale echo (n/n): Support simplified version of local echo local echo (n/n): Support simplified version of local echo Apr 2, 2025
@PIG208 PIG208 force-pushed the pr-echo branch 16 times, most recently from 988c615 to f297a65 Compare April 15, 2025 01:45
@PIG208 PIG208 marked this pull request as ready for review April 15, 2025 01:49
@PIG208 PIG208 force-pushed the pr-echo branch 5 times, most recently from aa81e2f to 28d545f Compare April 17, 2025 18:49
@PIG208 PIG208 changed the title local echo (n/n): Support simplified version of local echo local echo (6/6): Support simplified version of local echo Apr 17, 2025
@PIG208
Copy link
Member Author

PIG208 commented May 23, 2025

I temporarily removed the last two commits because the confirmation dialog requires us to rework the implementation and tests more.

Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Here's another round of review—it includes some comments I wrote yesterday on those commits that you temporarily removed in this revision, and they may or may not still apply to your new versions of those commits :)

@@ -163,6 +191,7 @@ mixin _MessageSequence {
/// Either the bottom slices of both [items] and [messages] are empty,
/// or the first item in the bottom slice of [items] is a [MessageListMessageItem]
/// for the first message in the bottom slice of [messages].
// TODO(#1453) update this in the context of outbox messages.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1453 is a PR number (this one) 🙂; did you mean to mention an issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is just a TODO for myself to update this dart doc, since we can have outbox-message related items in the list.

Comment on lines 477 to 480
// This loop relies on the assumption that all items that follow
// the last [MessageListMessageItem] are derived from outbox messages.
// If there is no [MessageListMessageItem] at all,
// this will end up removing end markers.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the second sentence still true/necessary after c5e6957?

Copy link
Member Author

@PIG208 PIG208 May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should end up removing all items, but that's less relevant now because we used to need to restore the end markers at the end. Updating it to describe the behavior accurately.

// TODO insert in middle instead, when appropriate
_addMessage(message);
if (event.localMessageId != null) {
final localMessageId = int.parse(event.localMessageId!);
// [outboxMessages] is epxected to be short, so removing the corresponding
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "expected"

Comment on lines 1406 to 1456
child: Text(message.senderFullName, // TODO(#716): use `store.senderDisplayName`
child: Text(message is Message
? store.senderDisplayName(message as Message)
: store.userDisplayName(message.senderId),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some of this that can go in a separate commit? I notice it deletes a TODO for a separate issue.

Comment on lines 1609 to 1663
/// A placeholder for Zulip message sent by the self-user.
///
/// See also [OutboxMessage].
class OutboxMessageWithPossibleSender extends StatelessWidget {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: how about

/// A "local echo" placeholder for a Zulip message to be sent by the self-user.

so it doesn't look like it could apply to messages that were sent in the past.

_SenderRow(message: message, showTimestamp: false),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
// This is adapated from [MessageContent].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "adapted"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Looks like I fixed in the wrong commit last time.

Comment on lines 629 to 665
test('not in narrow', () => awaitFakeAsync((async) async {
await prepare(narrow: eg.topicNarrow(stream.streamId, 'topic'), stream: stream);
await prepareMessages(foundOldest: true, messages:
List.generate(30, (i) => eg.streamMessage(stream: stream, topic: 'topic')));
await prepareOutboxMessages(count: 5, stream: stream);
check(model).outboxMessages.isEmpty();

async.elapse(kLocalEchoDebounceDuration);
checkNotNotified();
check(model).outboxMessages.isEmpty();
}));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prepareOutboxMessages defaults to a topic 'some topic', which is indeed different from 'topic'. But that's not easy to see locally; how about passing an explicit value that's easy to compare with 'topic' to see that it's different.

case OutboxMessageState.waitPeriodExpired:
final isComposeBoxOffered =
MessageListPage.ancestorOf(context).composeBoxState != null;
handleTap = isComposeBoxOffered ? () => _handlePress(context) : null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: _handleTap rather than _handlePress, I think, for consistency with GestureDetector.onTap

Also, to be consistent with edit-message, let's apply the handler only to the message content, not also the sender row.

Comment on lines +1711 to +1740
case OutboxMessageState.failed:
case OutboxMessageState.waitPeriodExpired:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional to show "MESSAGE NOT SENT" in the waitPeriodExpired state? I think this could cause a confusing symptom like #1525, where the message has actually been sent (or will be) but we say it hasn't.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In v0.0.29 we'd enter this state even if we got a success response from the send request, if the event didn't then show up. I suspect that's what happened in the report in #1525.

In the current revision of this PR, a success response will put the outbox-message into waiting state indefinitely, until the event arrives. I think showing this is OK if we've gotten neither the event nor a response from the request.

(The exact status in that case is more like "tried sending the message, it's been a while, haven't heard any response yet, so it may or may not have made it through, up to you if you want to retry". Not sure there's a good concise way to communicate that.)

case OutboxMessageState.waiting:
final designVariables = DesignVariables.of(context);
return Padding(
padding: const EdgeInsetsGeometry.only(top: 1.5, bottom: 0.5),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_OutboxMessageStatusRow is similar enough to _EditMessageStatusRow that I think it makes sense to be consistent between them in how we do this padding.

Experimentally, 0.5px on the bottom looks odd to me, as though it's meant to touch the bottom—and also the left and right edges of the screen—but it accidentally doesn't:

0.5px below 4px below (edit-message)
image image

I know your implementation follows the Figma, but in this case I would just do what we do for edit-message; the code is also simpler that way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we have adjusted the debounce timeout in the more recent version #1472, I think the issue with the layout shift caused by the loading indicator still exists (#1453 (comment)), at least for messages that don't have fancy formatting.

We have 4px total in message content's bottom padding, and the height of the progress bar is 2px. How about moving the progress bar to the top half of the bottom padding, so that there is 2px instead of 0.5px after it? Meanwhile, we can adjust the bottom padding of the edit status row to be 2px so that they are consistent.'

2px below 2px below (edit-message)
image image

Note that the local echo indicator sits inside the bottom padding of the message content, and the one for edit-message doesn't. I think this is fine since there is label that doesn't fit within the padding for edit-message anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, SGTM.

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the review cycles with Chris aren't yet complete, but I wanted to take a look in so you have a review for tomorrow. I made my way through the first four commits after #1472 (which this PR is stacked atop a version of):
67e5e1f msglist: Remove trailingWhitespace
827939a test [nfc]: Extract {dm,stream}OutboxMessage helpers
363024a msglist test [nfc]: Make checkInvariant compatible with MessageBase
b0dc888 msglist [nfc]: Extract _addItemsForMessage

and looked at part of the main model commit:
5579e00 msglist: Add and manage outbox message objects in message list view

Generally this looks good; comments below, all more or less small. Probably the priority for tomorrow is those two final commits that were temporarily removed (#1453 (comment)).

},
if (trailingWhitespace != null && item.isLastInBlock) SizedBox(height: trailingWhitespace!),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this feels like it makes things pretty crowded just above a recipient header.

msglist: Remove trailingWhitespace

This 11px whitespace can be traced back to 311d4d56e in 2022, and is not
present in the Figma design. It was there to match the web design while
prototyping the app.

I just spent a few minutes looking around in Figma and didn't find an example of a message list for an interleaved view, so one with messages followed by recipient headers. Do you know of one?

Probably it would be good to refine this spacing somehow, e.g. by skipping it when followed by the end of the feed rather than a recipient header. But let's not just take it out entirely. (And so for this PR, probably leave it the way it is, behavior-wise.)

It looks like in main it's already a constant. So the code could be simplified by propagating that constant down to this line, and therefore still removing the trailingWhitespace field.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only ones I have seen are in design-process drafts, like this one. I will turn this into an NFC for this PR.

check(narrow.containsMessage(
_TestDmMessage(allRecipientIds: [1, 2]))).isTrue();
eg.dmOutboxMessage(from: user1, to: [user2]))).isTrue();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check's data is inconsistent: the narrow says selfUserId is 2, but the message says it's 1.

(I guess this was present in the version already in main and this change is NFC. But the from/to makes it more apparent than the allRecipientIds[0] behavior did. That's a good thing about the new helpers. 🙂)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, right. The test was not set up correctly. Adding a prep commit (ideally we would change the old helpers to use from/to, but I kept it minimal just so we can fix the test data), so that we can keep the refactor NFC.

Comment on lines 2690 to 2697
if (message is Message) {
check(model.store.messages)[message.id].isNotNull().identicalTo(message);
} else if (message is OutboxMessage) {
check(message).hidden.isFalse();
check(model.store.outboxMessages)[message.localMessageId].isNotNull().identicalTo(message);
} else {
assert(false);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This first check can become two separate loops, making the logic of both of them simpler.

(Arguably it should have been a separate loop in the first place — it's checking something logically fairly different from what the rest of this loop body is checking. But that had less impact when it was one line.)

@@ -2151,17 +2684,27 @@ void checkInvariants(MessageListView model) {
check(model).fetchOlderCoolingDown.isFalse();
}

for (final message in model.messages) {
check(model.store.messages)[message.id].isNotNull().identicalTo(message);
final allMessages = [...model.messages, ...model.outboxMessages];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking through the existing checks on model.messages, here's another that should gain a counterpart for model.outboxMessages:

  check(isSortedWithoutDuplicates(model.messages.map((m) => m.id).toList()))
    .isTrue();

The outbox messages should be ordered by localMessageId.

Comment on lines 419 to 423
if (!messagesSameDay(prevMessageItem.message, message)) {
items.add(MessageListDateSeparatorItem(message));
canShareSender = false;
} else {
canShareSender = (prevMessageItem.message.senderId == message.senderId);
canShareSender = prevMessage.senderId == message.senderId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: keep this using prevMessageItem.message rather than prevMessage

The previous item is the thing that directly matters for whether this next item should share its sender row. It's also what's used a few lines above in the messagesSameDay call, so that keeps the logic a bit more internally coherent to read.

///
/// Usually this should not have that many items, so we do not anticipate
/// performance issues with unoptimized O(N) iterations through this list.
final List<OutboxMessage> outboxMessages = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds some overhead in magnitude of O(1) (where the constant is
the number of outbox messages in a view, expected to be small) on
message event handling.

That is not what the word "constant" means 🙂

Instead can say:

This adds some overhead on message-event handling, linear in the number of outbox messages in a view. We rely on that number being small.

Or could say something like "O(k), where there are k outbox messages in the view".

required super.showSender,
required super.isLastInBlock,
}) : content = ZulipContent(nodes: [
ParagraphNode(links: [], nodes: [TextNode(message.contentMarkdown)]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ParagraphNode(links: [], nodes: [TextNode(message.contentMarkdown)]),
ParagraphNode(links: null, nodes: [TextNode(message.contentMarkdown)]),

See the doc on the links field:

  /// A list of all [LinkNode] descendants.
  ///
  /// An empty list is represented as null.
  /// […]

@@ -718,7 +718,11 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
return MessageItem(
key: ValueKey(data.message.id),
header: header,
trailingWhitespace: 11,
item: data);
case MessageListOutboxMessageItem():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implements minimal support to display outbox message message item
widgets in the message list, without indicators for theirs states.
Retrieving content from failed sent requests and the full UI are
implemented in a later commit.

Is there — ah, I guess that was in the commits you temporarily removed as mentioned at #1453 (comment) .

PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request May 27, 2025
This 11px whitespace can be traced back to 311d4d5 in 2022.

While this is not present in the Figma design, it would be a
good idea to refine it in the future.  See discussion:
  zulip#1453 (comment)
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request May 27, 2025
This changes the bottom padding from 4px to 2px, to match the later
added status row for outbox messages.  See discussion:
  zulip#1453 (comment)
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request May 27, 2025
Different from the Figma design, the bottom padding below the progress
bar is changed from 0.5px to 2px, as discussed here:
  zulip#1453 (comment)

Fixes: zulip#1441
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request May 27, 2025
This changes the bottom padding from 4px to 2px, to match the later
added status row for outbox messages.  See discussion:
  zulip#1453 (comment)
PIG208 added a commit to PIG208/zulip-flutter that referenced this pull request May 27, 2025
Different from the Figma design, the bottom padding below the progress
bar is changed from 0.5px to 2px, as discussed here:
  zulip#1453 (comment)

Fixes: zulip#1441
@PIG208
Copy link
Member Author

PIG208 commented May 27, 2025

Thanks for the reviews! Pushed an update, this time with the commits I removed last time, with a bunch of prep commits. I also went through this PR's interaction with middleMessage/middleItem, and implemented the behavior discussed in a prior call with Greg, making sure that middleItem points to MessageListOutboxMessageItem in certain condition (the docs on middleItem has been updated to reflect the behavior I intended and implemented).

@PIG208
Copy link
Member Author

PIG208 commented May 27, 2025

Some visual changes in this update:

screenshots

The error message for discarding draft on restoring a message not sent is tentative:

When you restore a message not sent, the content that was previously in the compose box is discarded.

The bottom padding of the status row has changed:

2px below 2px below (edit-message)
image image

from #1453 (comment)

@gnprice
Copy link
Member

gnprice commented May 28, 2025

Thanks for the revision! The changes related to my review at #1453 (review) all look good, and the initial 5 commits all look good to me now:
c1f6b8d msglist [nfc]: Make trailingWhitespace a constant
0b12cab narrow test: Make sure sender is selfUser in tests
e1fa25e test [nfc]: Extract {dm,stream}OutboxMessage helpers
33a2e0d msglist test [nfc]: Make checkInvariant compatible with MessageBase
93d89a6 msglist [nfc]: Extract _addItemsForMessage

I haven't yet read the subsequent commits in full:
5488a7a msglist: Use store.senderDisplayName for sender row
d64698e msglist [nfc]: Make _SenderRow accept MessageBase
9c0b955 msglist: Add and manage outbox message objects in message list view
6604cf4 compose [nfc]: Make confirmation dialog message flexible
b2d6c50 compose test [nfc]: Move some edit message helpers out of group
cfbbdc0 compose [nfc]: Update string to mention "edit" in name and description
c8295ae msglist: Adjust edit-message status row bottom padding
1fa9f24 msglist: Support retrieving failed outbox message content

@chrisbobbe will be taking over this PR; I'll leave those latter commits until after he's made a pass through them.

@chrisbobbe
Copy link
Collaborator

Great! Merging those first five, with a small commit-message tweak:

$ git range-diff upstream 93d89a6a a4c564b1c
1:  c1f6b8dca = 1:  86f62c9b1 msglist [nfc]: Make trailingWhitespace a constant
2:  0b12cab63 ! 2:  75f0debbf narrow test: Make sure sender is selfUser in tests
    @@ Metadata
     Author: Zixuan James Li <[email protected]>
     
      ## Commit message ##
    -    narrow test: Make sure sender is selfUser in tests
    +    narrow test: Make sure sender is selfUser for outbox DM messages
    +
    +    [chris: expanded commit-message summary line]
    +
    +    Co-authored-by: Chris Bobbe <[email protected]>
     
      ## test/model/narrow_test.dart ##
     @@ test/model/narrow_test.dart: void main() {
3:  e1fa25e8f = 3:  815b9d2f4 test [nfc]: Extract {dm,stream}OutboxMessage helpers
4:  33a2e0d04 = 4:  2829bd847 msglist test [nfc]: Make checkInvariant compatible with MessageBase
5:  93d89a6ac = 5:  a4c564b1c msglist [nfc]: Extract _addItemsForMessage

chrisbobbe pushed a commit that referenced this pull request May 28, 2025
This 11px whitespace can be traced back to 311d4d5 in 2022.

While this is not present in the Figma design, it would be a
good idea to refine it in the future.  See discussion:
  #1453 (comment)
chrisbobbe pushed a commit to chrisbobbe/zulip-flutter that referenced this pull request May 28, 2025
This is where the progress bar for outbox messages will go, so this
is for consistency with that. Discussion:
  zulip#1453 (comment)
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request May 28, 2025
This is where the progress bar for outbox messages will go, so this
is for consistency with that. Discussion:
  zulip#1453 (comment)

[chris: fixed to maintain 4px bottom padding in the common case
where the progress bar is absent]

Co-authored-by: Chris Bobbe <[email protected]>
@chrisbobbe
Copy link
Collaborator

chrisbobbe commented May 28, 2025

And I've sent revised versions of the next chunk, in #1535.

chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request May 29, 2025
This is where the progress bar for outbox messages will go, so this
is for consistency with that. Discussion:
  zulip#1453 (comment)

[chris: fixed to maintain 4px bottom padding in the common case
where the progress bar is absent]

Co-authored-by: Chris Bobbe <[email protected]>
PIG208 and others added 5 commits May 30, 2025 14:27
This adds some overhead on message-event handling, linear in the
number of outbox messages in a view. We rely on that number being
small.

We add outboxMessages as a list independent from messages on
_MessageSequence.  Because outbox messages are not rendered
(the raw content is shown as plain text), we leave the 1-1
relationship between `messages` and `contents` unchanged.

When computing `items`, we now start to look at `outboxMessages` as
well, with the guarantee that the items related to outbox messages
always come after those for other messages.  Look for places that call
`_processOutboxMessage(int index)` for references, and the changes to
`checkInvariants` on how this affects the message list invariants.

`addOutboxMessage` is similar to `handleMessage`.  However, outbox
messages do not rely on the fetched state, i.e. they can be
synchronously updated when the message list view was first
initialized.

This implements minimal support to display outbox message message item
widgets in the message list, without indicators for theirs states.
Retrieving content from failed sent requests and the full UI are
implemented in a later commit.

Co-authored-by: Chris Bobbe <[email protected]>
Issue zulip#720 is superseded by zulip#1441, in which we'll still clear the
compose box when the send button is tapped. (We'll still preserve
the composing progress in case the send fails, but we'll do so in an
OutboxMessage instead of within the compose input in a disabled
state.)
Issue zulip#720 is superseded by zulip#1441, and these don't apply... I guess
with the exception of a note on how a generic "x" button could be
laid out, so we leave that.
Different from the Figma design, the bottom padding below the progress
bar is changed from 0.5px to 2px, as discussed here:
  zulip#1453 (comment)

Fixes: zulip#1441
Co-authored-by: Chris Bobbe <[email protected]>
@chrisbobbe
Copy link
Collaborator

OK, and now rebased with a revision ready for review! This is stacked atop #1538, which is a separate PR because it's a substantial commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
maintainer review PR ready for review by Zulip maintainers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplified local echo, with retry
4 participants